package spitfire.ksim.node;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import spitfire.ksim.algorithm.FuzzyRule;
import spitfire.ksim.algorithm.RankList;
import spitfire.ksim.algorithm.RuleExtractionAlgorithm;
import spitfire.ksim.algorithm.RuleExtractionImpl;
import spitfire.ksim.data.KSensorSnapshot;
import spitfire.ksim.data.TSDEstablishData;
import spitfire.ksim.data.TSDRequestData;
import spitfire.ksim.data.TSDResponseData;
import spitfire.ksim.data.TSDUpdateData;
import spitfire.ksim.network.KNetAddress;
import spitfire.ksim.network.KNetwork;
import spitfire.ksim.simulator.KJob;
import spitfire.ksim.simulator.KSimDuration;
import spitfire.ksim.simulator.KSimulator;
import spitfire.ksim.util.TPoint;

/**
 * <p>
 * KSensorNode represents a node with several type of sensors.
 * A sensor node has an sensor ID which is defined in the sensor node profile, 
 * and a semantic description. It can provides a snapshot of sensed data of 
 * all the sensors in it. It is also responsible for loading the sensor node 
 * profile and instantiating KSensor objects with data in the profile. 
 * </p>
 * 
 * @author Adam
 */
public class KSensorNode extends KNode {

    public static final long DEFAULT_SNAPSHOT_LEN = 24 * 3600 * 1000; //24 h
    private long timeWindow = 24 * 3600 * 1000;
    public static long commonTimeWindow = 24 * 3600 * 1000; //Default 24h
    private int timeWindowOption = 1;   // 1 = common TW / 2 = own TW
	private String nodeId;
	private String dataPath; 
	private String description = "Unannotated";
	private List<KSensor> sensorList = new ArrayList<KSensor>();
	private RuleExtractionAlgorithm extrAlgo = new RuleExtractionImpl();
	
	private List<RankListRecvListener> rankListRecvListenerList =
			new ArrayList<RankListRecvListener>();
	
	public KSensorNode(KSimulator simulator, KNetwork network, TPoint location) {
		super(simulator, network, location);
		timeWindow = DEFAULT_SNAPSHOT_LEN;
	}
	
	//by Danilo
	public void setTimeWindow(long value){
		timeWindow = value;
	}
	public long getTimeWindow(){
		return timeWindow;
	}
	
//	public static void setCommonTimeWindow(long value){
//		commonTimeWindow = value;
//	}
//	public static long getCommonTimeWindow(){
//		return commonTimeWindow;
//	}
	
	public void setTimeWindowOption(int value){
		timeWindowOption = value;
	}
	public long getTimeWindowOption(){
		return timeWindowOption;
	}
	//##
	
	@Override
	public synchronized void doJob(KJob job) {
//		System.out.println("Doing Job(id=" + job.getJobId() + "), addr: " + getNetAddr());
		String jobType = (String)job.getProperty("type");
		if (jobType == null) {
			return;
		} else if (jobType.equalsIgnoreCase("SD_ESTABLISH")) {
			KNetAddress addr = (KNetAddress) job.getProperty("target");
			KJob nextJob = new KJob("6:0:0");
			nextJob.addProperty("type", "SD_ESTABLISH");
			nextJob.addProperty("target", addr);
			addOwnJob(nextJob);
			// if some sensor annotated and has enough data, Send establish data and add SD_UPDATE job for future
			if (addr != null) {
				TSDEstablishData establishData = formDefaultEstablishData();
				if (establishData.getData().size() > 0) {
					send(addr, establishData);
				}
			}
		} else if (jobType.equalsIgnoreCase("SD_UPDATE")) {
			KNetAddress addr = (KNetAddress) job.getProperty("target");
			KJob nextJob = new KJob("6:0:0");
			nextJob.addProperty("type", "SD_UPDATE");
			nextJob.addProperty("target", addr);
			addOwnJob(nextJob);
			if (addr != null) {
				TSDUpdateData updateData = formDefaultUpdateData();
				if (updateData.getData().size() > 0) {
					send(addr, updateData);
				}
			}
		} else if (jobType.equalsIgnoreCase("SD_REQUEST")) {
			KNetAddress addr = (KNetAddress) job.getProperty("target");
			KJob nextJob = new KJob("8:0:0");
			nextJob.addProperty("type", "SD_REQUEST");
			nextJob.addProperty("target", addr);
			addOwnJob(nextJob);
			if (addr != null) {
				TSDRequestData requestData = formDefaultRequestData();
				if (requestData.getData().size() > 0) {
					send(addr, requestData);
				}
			}
		}
	}

	public KSimDuration getDataDuration() {
		long startTime = Long.MAX_VALUE;
		long endTime = 0;
		synchronized (sensorList) {
			for (KSensor sensor : sensorList) {
				List<Long> timeStampList = sensor.getTimeStampList();
				if (!timeStampList.isEmpty()) {
					if (startTime > timeStampList.get(0)) {
						startTime = timeStampList.get(0);
					}
					if (endTime < timeStampList.get(timeStampList.size() - 1)) {
						endTime = timeStampList.get(timeStampList.size() - 1);
					}
				}
			}
		}
		if (startTime < endTime) {
			return new KSimDuration(startTime, endTime);
		} else {
			return null;
		}
	}

	@Override
	public synchronized void onPacketRecv(KNetAddress srcAddr, Object payload) {
		System.out.println("[" + DateFormat.getDateTimeInstance().format(getSimTime())
				+ "] " + getDisplayName() + " Recv from " + srcAddr + ": " + payload);
		if (payload instanceof TSDResponseData) {
			TSDResponseData responseData = (TSDResponseData) payload;
			for (KSensor sensor : sensorList) {
				RankList rankList = responseData.getRankList(sensor.getSensorType());
				if (rankList != null) {
					invokeRankListRecv(sensor, rankList);
				}
//				String sd = responseData.getSensorSd(sensor.getSensorType());
//				if (sd != null) {
//					sensor.setDescription(sd);
//				}
			}
		}
	}
	
	/**
	 * Load sensor node profile from the given data path and instantiate sensors
	 * of different types according to the profile.
	 * 
	 * @param dataPath - path of the sensor node profile
	 */
	public void loadData(String dataPath) {
		this.dataPath = dataPath;
		
		try {
			//Read the data from file
			BufferedReader reader = new BufferedReader(new InputStreamReader(
					new FileInputStream(dataPath), "UTF-8"));
			//Read node ID and make up display name
			String line = reader.readLine();
			setNodeId(line.split("§")[1]);
			setDisplayName(getNodeId() + "|" + getNetAddr());
			// Read node SD
			line = reader.readLine();
			// Read sensor number
			line = reader.readLine();
			int sensorNum = Integer.parseInt(line.split("§")[1]);
			
			// For each sensor
			int currentSensorIdx = -1;
			KSensor currentSensor = null;
			String sensorType = null;
			String[] tokens = null;
			Long timeStamp = null;
			Double value = null;
			while ((line = reader.readLine()) != null) {
				if (line.startsWith("SensorType§")) {
					// New sensor
					currentSensorIdx++;
					if (currentSensorIdx >= sensorNum) {
						throw new IOException("Declared number of sensor" +
								" doesn't consist with actual number of sensors");
					}
					// Get sensor type
					sensorType = line.substring("SensorType§".length());
					currentSensor = new KSensor(sensorType, this);
					// Add the sensor to this node
					synchronized (sensorList) {
						sensorList.add(currentSensor);
					}
					continue;
				}
				if (line.startsWith("SensorSD§")) {
					// Read sensor SD if exists
					currentSensor.setDescription(line.substring("SensorSD§".length()));
					continue;
				}
			
				tokens = line.split("§", -1);
				// Skip missing data
				if (tokens[0].equals("") || tokens[1].equals("")) {
					continue;
				}
				timeStamp = Long.valueOf(tokens[0]);
				value = Double.valueOf(tokens[1]);
				currentSensor.addTimeAndData(timeStamp, value);
			}
			reader.close();
		} catch (Exception e) {
			System.out.println("Error while processing sensor data file " + dataPath);
			e.printStackTrace();
		}
	}
	
	public void setNodeId(String nodeId) {
		this.nodeId = nodeId;
		invokeStateChange();
	}
	
	public String getNodeId() {
		return nodeId;
	}
	
	public String getDatapath() {
		return dataPath;
	}
	
	public void setDescription(String description) {
		synchronized(this.description) {
			this.description = description;
		}
		invokeStateChange();
	}

	public String getDescription() {
		synchronized(description) {
			return description;
		}
	}
	
	/**
	 * Form establish data with {@link KSensorSnapshot} taken from the given 
	 * start time to the given end time.
	 * 
	 * @param fromTime - start time of sensor snapshot.
	 * @param toTime - end time of sensor snapshot.
	 * @return TSDEstablishData from given start time to the given end time.
	 */
	public TSDEstablishData formEstablishData(long fromTime, long toTime) {
		TSDEstablishData establishData = new TSDEstablishData(getNodeId());
		for (KSensor sensor : getSensorList()) {
			if (sensor.isPowerOn()
					&& !sensor.getDescription().equals("Unannotated")
					&& !sensor.isRuleEstablished()) {
				KSensorSnapshot sensorSnapshot = sensor.takeSnapshot(fromTime, toTime);
				if (sensor.isSufficient(sensorSnapshot)) {
					establishData.addSensorSnapshot(sensor.getSensorType(),
							sensor.getDescription(), sensorSnapshot);
					sensor.setRuleEstablished(true);
				}
			}
		}
		return establishData;
	}

	/**
	 * Form update data with {@link KSensorSnapshot} taken from the given 
	 * start time to the given end time.
	 * 
	 * @param fromTime - start time of sensor snapshot.
	 * @param toTime - end time of sensor snapshot.
	 * @return TSDUpdateData from given start time to the given end time.
	 */
	public TSDUpdateData formUpdateData(long fromTime, long toTime) {
		TSDUpdateData updateData = new TSDUpdateData(getNodeId());
		for (KSensor sensor : getSensorList()) {
			if (sensor.isPowerOn()
					&& !sensor.getDescription().equals("Unannotated")
					&& sensor.isRuleEstablished()) {
				KSensorSnapshot sensorSnapshot = sensor.takeSnapshot(fromTime, toTime);
				if (sensor.isSufficient(sensorSnapshot)) {
					List<Long> timeStampList = sensorSnapshot.getTimeStampList();
					List<Double> dataList = sensorSnapshot.getDataList();
					FuzzyRule rule = extrAlgo.extractRule(timeStampList, dataList);
					updateData.addSensorSnapshot(sensor.getSensorType(),
							sensor.getDescription(), rule);
				}
			}
		}
		return updateData;
	}

	/**
	 * Form request data with {@link KSensorSnapshot} taken from the given 
	 * start time to the given end time.
	 * 
	 * @param fromTime - start time of sensor snapshot.
	 * @param toTime - end time of sensor snapshot.
	 * @return TSDRequestData from given start time to the given end time.
	 */
	public TSDRequestData formRequestData(long fromTime, long toTime) {
		TSDRequestData requestData = new TSDRequestData(getNodeId());
		for (KSensor sensor : getSensorList()) {
			if (sensor.isPowerOn() && sensor.getDescription().equals("Unannotated")) {
				KSensorSnapshot sensorSnapshot = sensor.takeSnapshot(fromTime, toTime);
				if (sensor.isSufficient(sensorSnapshot)) {
					requestData.addSensorSnapshot(sensor.getSensorType(), sensorSnapshot);
				}
			}
		}
		return requestData;
	}

	private long get6oclockInMillis() {
		long simTime = getSimTime();
		Calendar c = Calendar.getInstance();
		c.setTimeInMillis(simTime);
		c.set(Calendar.HOUR_OF_DAY, 6);
		c.set(Calendar.MINUTE, 0);
		c.set(Calendar.SECOND, 0);
		c.set(Calendar.MILLISECOND, 0);
		return c.getTimeInMillis();
	}
	
	/**
	 * Form {@link TSDEstablishData} from 6 o'clock yesterday to 6 o'clock today.
	 * @return {@link TSDEstablishData} from 6 o'clock yesterday to 6 o'clock today.
	 */
	public TSDEstablishData formDefaultEstablishData() {
		long snapshotTime = get6oclockInMillis();
		if(timeWindowOption == 1){
			return formEstablishData(snapshotTime - commonTimeWindow, snapshotTime);
		} else if(timeWindowOption == 2){
			return formEstablishData(snapshotTime - timeWindow, snapshotTime);
		} else {
			throw new RuntimeException();
		}
	}
	
	/**
	 * Form {@link TSDUpdateData} from 6 o'clock yesterday to 6 o'clock today.
	 * @return {@link TSDUpdateData} from 6 o'clock yesterday to 6 o'clock today.
	 */
	public TSDUpdateData formDefaultUpdateData() {
		long snapshotTime = get6oclockInMillis();
		if(timeWindowOption == 1){
			return formUpdateData(snapshotTime - commonTimeWindow, snapshotTime);
		} else if(timeWindowOption == 2){
			return formUpdateData(snapshotTime - timeWindow, snapshotTime);
		} else {
			throw new RuntimeException();
		}
	}

	/**
	 * Form {@link TSDRequestData} from 6 o'clock yesterday to 6 o'clock today.
	 * @return {@link TSDRequestData} from 6 o'clock yesterday to 6 o'clock today.
	 */
	public TSDRequestData formDefaultRequestData() {
		long snapshotTime = get6oclockInMillis();
		if(timeWindowOption == 1){
			return formRequestData(snapshotTime - commonTimeWindow, snapshotTime);
		} else if(timeWindowOption == 2){
			return formRequestData(snapshotTime - timeWindow, snapshotTime);
		} else {
			throw new RuntimeException();
		}
	}

	/**
	 * @return the list of sensors in this node.
	 */
	public List<KSensor> getSensorList() {
		return sensorList;
	}
	
	public void addRankListRecvListener(RankListRecvListener l) {
		rankListRecvListenerList.add(l);
	}
	
	public void removeRankListRecvListener(RankListRecvListener l) {
		rankListRecvListenerList.remove(l);
	}
	
	public void invokeRankListRecv(KSensor sensor, RankList rankList) {
		synchronized (rankListRecvListenerList) {
			for (RankListRecvListener l : rankListRecvListenerList) {
				l.onRankListRecv(sensor, rankList);
			}
		}
	}
}
